package start.spring.basic.util.common;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Base64;

import lombok.extern.slf4j.Slf4j;

/**
 * 
 * ****************************************************************************
 * MFA Authenticator
 *
 * @author(作者)：xuyongyun	
 * @date(创建日期)：2023年4月1日
 * 
 * @Link：https://blog.csdn.net/weixin_42592282/article/details/121971626
 ******************************************************************************
 */
@Slf4j
public class MfaAuthenticator {
	
	// taken from Google pam docs - we probably don't need to mess with these
    public static final int SECRET_SIZE = 10;

    public static final String SEED = "GlUYNzKFI7izOF8GwLDVKs2m0QN7vxRs2im5MDaNCWGmcD2rvcZxg8GjEvTbW5oVSV7avLBdwIHq";

    public static final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";

    //default 3 - max 17 (from google docs)最多可偏移的时间
    public static final int WINDOW_SIZE_DEFAULT = 3;

    /**
     * 生成密钥
     *	
     * @param userId 用户id，不需要时传入null
     * 
     * @return
     * @since           1.0
     */
    public static String genSecret(String userId) {
        SecureRandom sr = null;
        try {
        	StringBuilder sBuilder = new StringBuilder();
        	if(!StringUtil.isEmpty(userId)) {
        		sBuilder.append(userId);
        	}
        	sBuilder.append(SEED);
        	sBuilder.append(System.currentTimeMillis());
        	
            sr = SecureRandom.getInstance(RANDOM_NUMBER_ALGORITHM);
            sr.setSeed(Base64.decodeBase64(sBuilder.toString()));
            byte[] buffer = sr.generateSeed(SECRET_SIZE);
            Base32 codec = new Base32();
            byte[] bEncodedKey = codec.encode(buffer);
            String encodedKey = new String(bEncodedKey);
            return encodedKey;
        } catch (NoSuchAlgorithmException e) {
            log.error("", e);
        }
        return null;
    }

    /**
     * 生成绑定用url
     *	
     * @param userId 用户id
     * @param host 域名
     * @param secret 密钥
     * @return
     * @since           1.0
     */
    public static String getQrcodeUrl(String userId, String secret) {
        String format = "otpauth://totp/%s?secret=%s";
        return String.format(format, userId, secret);
    }
    
    /**
     * 校验动态验证码是否合法
     *	
     * @param secret 密钥
     * @param code 动态验证码
     * @param windowSize 允许误差的时间窗口，每个时间窗口30秒，传入null时采用默认值3
     * 
     * @return
     * @since           1.0
     */
    public static boolean checkCode(String secret, long code, Integer windowSize) {
    	int window_size = WINDOW_SIZE_DEFAULT;
    	if(windowSize!=null && windowSize>=1 && windowSize<=17) {
    		window_size = windowSize;
    	}
    	
        Base32 codec = new Base32();
        byte[] decodedKey = codec.decode(secret);
        // convert unix msec time into a 30 second "window"
        // this is per the TOTP spec (see the RFC for details)
        long timeMsec = System.currentTimeMillis();
        long t = (timeMsec / 1000L) / 30L;
        // Window is used to check codes generated in the near past.
        // You can use this value to tune how far you're willing to go.
        for (int i = -window_size; i <= window_size; ++i) {
            long hash;
            try {
                hash = calculateCode(decodedKey, t + i);
            } catch (Exception e) {
                log.error("", e);
                return false;
            }
            if (hash == code) {
                return true;
            }
        }
        return false;
    }
    
    /**
     * 计算指定时间窗口对应的动态码
     *	
     * @param key 密钥的Base32编码
     * @param t 时间戳
     * 
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     * @since           1.0
     */
    private static int calculateCode(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException {
        byte[] data = new byte[8];
        long value = t;
        for (int i = 8; i-- > 0; value >>>= 8) {
            data[i] = (byte) value;
        }
        SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA1");
        Mac mac = Mac.getInstance("HmacSHA1");
        mac.init(signKey);
        byte[] hash = mac.doFinal(data);
        int offset = hash[20 - 1] & 0xF;
        // We're using a long because Java hasn't got unsigned int.
        long truncatedHash = 0;
        for (int i = 0; i < 4; ++i) {
            truncatedHash <<= 8;
            // We are dealing with signed bytes:
            // we just keep the first byte.
            truncatedHash |= (hash[offset + i] & 0xFF);
        }
        truncatedHash &= 0x7FFFFFFF;
        truncatedHash %= 1000000;
        return (int) truncatedHash;
    }
    
//    public static void main(String[] args) {
//    	String userId = "testuser";
//    	String host = "testhost";
//        String secret = MfaAuthenticator.genSecret(userId);
//        String qrBarcodeURL = MfaAuthenticator.getQrcodeUrl(userId, host, secret);
//        System.out.println("genSecret:" + secret);
//        System.out.println("getQRBarcodeURL:" + qrBarcodeURL);
////        System.out.println(ma.authcode("345126", secret));
//    }
}
